﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using CSJ2K.Util;

namespace CSJ2K.Util.IO
{
    /*
     * Tile-Part marker blocks contain a specially packed bitstream of data about
     * the tile that is only byte-aligned occasionally.  This class allows reading
     * non-byte aligned bits, bytes and integers from the bitstream.  
     * 
     * The bitstream length is known before reading begins so an endPosition can 
     * be passed in to limit how far it will read before throwing an exception.
     * This is primarily for debugging purposes as the code that parses the
     * bitstream shouldn't ever try to read past the end unless it's buggy.
     * The alternative would be to read the data into a byte array and accessing
     * memorystream but the bitstreams can be very large so it wouldn't be memory 
     * efficient.
     */
    public class BitstreamReader
    {
        internal Stream Source;
        long endPosition;

        internal byte[] Buffer = new byte[2];

        byte BitPosition = 0;

        // Masks for individual bits ordered from MSB to LSB
        static byte[] bitmasks = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };

        public BitstreamReader(byte[] Bytes) : this(new MemoryStream(Bytes)) { }
        internal BitstreamReader(Stream Source) : this(Source, long.MaxValue) { }
        internal BitstreamReader(Stream Source, long endPosition)
        {
            this.Source = Source;
            this.endPosition = endPosition;
        }

        // jump to the start of the next byte to skip byte-alignment padding unless we are
        // only at bit 0.
        public byte Align()
        {
            byte padbits = 0;
            if (BitPosition > 0)
            {
                padbits = (byte)(8 - BitPosition);
                // this is inefficient but helps considerably with debugging.
                for (int i = 0; i < padbits; i++)
                {
                    if (DEBUG.BitstreamReader_Enforce0Padding)
                    {
                        if (this.ReadBit())
                            throw (new ApplicationException("Padding bit " + (i + 1) + " of " + padbits + " is non-zero. Bit position was " + (BitPosition == 0 ? 7 : BitPosition - 1) + ". Byte was 0x" + Buffer[0].ToString("X2") + "."));
                    }
                    else
                    {
                        if (this.ReadBit())
                            Debug.WriteLine("Padding bit " + (i + 1) + " of " + padbits + " is non-zero. Bit position was " + (BitPosition == 0 ? 7 : BitPosition - 1) + ". Byte was 0x" + Buffer[0].ToString("X2") + ".");
                    }
                }
                if (BitPosition > 0) throw (new ApplicationException("Alignment Error"));
            }
            return (padbits);
        }

        internal bool ReadBit()
        {
            if (DEBUG.BitstreamReader) Debug.Write("<" + BitPosition + ">");
            // Read a new byte if we need one.
            if (BitPosition == 0)
            {
                if (Source.Position >= endPosition) throw (new EndOfStreamException("Attempting to read past endPosition."));
                Buffer[1] = Buffer[0];
                if (Source.Read(Buffer, 0, 1) < 1) throw (new EndOfStreamException());
                if (DEBUG.BitstreamReader) Debug.Write("{0x" + Buffer[0].ToString("X2") + ",0x" + Buffer[1].ToString("X2") + "}");
                // Check for required padding bit.
                if (Buffer[1] == 0xFF)
                {
                    if (DEBUG.BitstreamReader) Debug.Write("(Pad Bit)");
                    if ((Buffer[0] & bitmasks[BitPosition]) == bitmasks[BitPosition])
                        throw (new ApplicationException("Padding bit is non-zero."));
                    BitPosition++;
                }
            }

            // Read the value of the current bit.
            bool bit = false;
            if ((Buffer[0] & bitmasks[BitPosition]) == bitmasks[BitPosition]) bit = true;

            // Increment to the next bit position.
            BitPosition++;
            if (BitPosition >= 8) BitPosition = 0;

            return bit;
        }

        internal byte ReadOctet(byte len)
        {
            if (len > 8) throw (new ApplicationException("Requesting too many bits."));
            byte output = 0;
            for (int i = 0; i < len; i++)
            {
                output <<= 1;
                if (ReadBit()) output |= 0x01;
            }
            return output;
        }
        internal byte ReadRawOctet()
        {
            if (BitPosition != 0) throw (new ApplicationException("Trying to read a raw byte without aligning to byte boundary first."));
            if (Source.Position >= endPosition) throw (new EndOfStreamException("Attempting to read past endPosition."));
            Buffer[1] = Buffer[0];
            if (Source.Read(Buffer, 0, 1) < 1) throw (new EndOfStreamException());
            return Buffer[1];
        }

        internal byte[] ReadRawOctets(int count)
        {
            if (BitPosition != 0) throw (new ApplicationException("Trying to read a raw byte without aligning to byte boundary first."));
            if (Source.Position >= endPosition) throw (new EndOfStreamException("Attempting to read past endPosition."));

            byte[] bytes = new byte[count];
            if(count>0)
            {

                bytes[0] = Buffer[0];
                if (count > 1)
                {
                    Source.Read(bytes, 1, count - 1);
                }
                Buffer[1] = bytes[count-1];
                if (Source.Read(Buffer, 0, 1) < 1) throw (new EndOfStreamException());
            }
            return bytes;
        }

        internal ushort ReadUInt16(byte len)
        {
            if (len > 16) throw (new ApplicationException("Requesting too many bits."));
            ushort output = 0;
            for (int i = 0; i < len; i++)
            {
                output <<= 1;
                if (ReadBit()) output |= 0x0001;
            }
            return output;
        }
        internal uint ReadUInt32(byte len)
        {
            if (len > 32) throw (new ApplicationException("Requesting too many bits."));
            uint output = 0;
            for (int i = 0; i < len; i++)
            {
                output <<= 1;
                if (ReadBit()) output |= 0x00000001;
            }
            return output;
        }
        internal ulong ReadUInt64(byte len)
        {
            if (len > 64) throw (new ApplicationException("Requesting too many bits."));
            ulong output = 0;
            for (int i = 0; i < len; i++)
            {
                output <<= 1;
                if (ReadBit()) output |= 0x0000000000000001;
            }
            return output;
        }
    }
}
